/*  MLService.Java - MicroLocation Framework Service - 
 *  Check location using Wifi signals
        Copyright (C) 2009 Devendra Laulkar

        This program is free software: you can redistribute it and/or modify
        it under the terms of the GNU General Public License as published by
        the Free Software Foundation version 2 of the License. 

        This program is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
        GNU General Public License for more details.

        You should have received a copy of the GNU General Public License
        along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package com.intellBatt.services;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

import com.intellBatt.LocationDbAdapter;
import com.intellBatt.LocationModel;
import com.intellBatt.WifiNetwork;

public class MLService extends Service {

	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}

	public static final String KEY_NAME = "name";
	public static final String KEY_WIFI_SCAN_PERIOD = "wscanfp";
	public static final String KEY_NUMSCANS = "numscans";
	public static final String KEY_ID = "idd";
	public static final String KEY_SCANRETURN = "scanreturn";
	public static final String KEY_AUTOTRAIN = "at";
	public static final String KEY_PROB = "prob";
	public static final String KEY_COUNT= "cnt";
	public static final String KEY_LASTSEEN = "lst";
	
	private boolean registered, inTraining, inChecking, inRetrain;
	LocationModel model;
	Intent retIntent;
	LocationDbAdapter dbhelper;
	private boolean dbLoaded = false;

	private HashMap<String, LocationModel> locations;

	/* Defaults */
	public static final int DEFAULT_SCAN_PERIOD = 5000;
	public static final int DEFAULT_NUM_SCANS = 1;
	private static final double MIN_PROB = 0.01;



	/* Wifi scanning related code */
	private WifiManager wifiManager = null;
	private Date nextScan; /* TODO - this needs to be synchronised ? */
	private  int wifiScanPeriod = DEFAULT_SCAN_PERIOD;
	private int numScans = DEFAULT_NUM_SCANS;
	private int scansDone = 0;
	private IntentFilter scanResultIntentFilter = new IntentFilter();

	private final BroadcastReceiver scanResult = new BroadcastReceiver() {

		@Override
		public void onReceive(Context context, Intent intent) 
		{

			
			//Log.i("onReceive", "got stuff");
			if (!intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) 
			{
				//Log.w("AADscanResult", "Got an invalid intent");
				return;
			}

			List<ScanResult> results = wifiManager.getScanResults();

			for(ScanResult nw : results)
			{
				if(model != null)
				{
					model.addSignalReading(nw.BSSID, nw.level);
				}
				else
				{
					Log.i("scanresult", "Trainer not set");
					break;
				}

				//Log.i("Wifi:", nw.SSID + "," + nw.BSSID + "," + nw.level + "," + nw.frequency + "," + nw.capabilities);
			}

			scansDone++;
			if(scansDone >= numScans)
			{
				log("All scans done");
				completedScanning();
			}
			else
			{
				// Schedule Next scan 
				log("Schduling new scan");
				scheduleScan(new Date());

			}
		}
	};


	private void scheduleScan(Date lastScan)
	{
		if(lastScan.getTime() + wifiScanPeriod > nextScan.getTime() )

		{
			TimerTask timerTask =  new TimerTask() 
			{

				@Override
				public void run() {
					scanWifi();
				}
			};
			timer.cancel();
			timer = new Timer();
			nextScan = new Date(lastScan.getTime() + wifiScanPeriod);
			timer.schedule(timerTask, nextScan);
		}
	}
	private void scanWifi()
	{
		if(wifiManager != null)
			wifiManager.startScan();
		else
		{
			wifiManager = (WifiManager)getSystemService(WIFI_SERVICE);
			wifiManager.startScan();
		}
	}

	private Timer timer = new Timer();
	/* End of Wifi scanning related code */

	private BroadcastReceiver locReceiver = new BroadcastReceiver(){

		public void onReceive(Context ctx, Intent mIntent)
		{
			log( "Got request, processing");
			runMain(mIntent);
		}

	};

	public void onStart(Intent intent, int code)
	{
		super.onStart(intent, code);
		this.registerReceiver(locReceiver, new IntentFilter("com.microlocation.services.LOCATION"));
		if(wifiManager == null)
		{
			wifiManager = (WifiManager)getSystemService(WIFI_SERVICE);
			scanResultIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
		}
		log("onStart");
		runMain(intent);
	}

	public void runMain(Intent mIntent)
	{

		Bundle extras = mIntent.getExtras();

		if(extras == null)
		{
			log("Extras are null");
			return;
			
		}

		String action = extras.getString("action");

		if(action == null || "".equals(action))
		{
			log("Action is null");
			return;
		}
		String retString = extras.getString("returnIntent");
		if(retString == null || "".equals(retString))
		{
			log("No return intent specified");
			return;
		}

		retIntent = new Intent(retString);
		// Copy bundle
		retIntent.putExtras(extras);
		model = new LocationModel();
		if("train".equalsIgnoreCase(action))
		{
			Log.i("MLSERIVCE", "Got training request");
			inTraining = true;
			inChecking = false;
			inRetrain = false;
			model.setLocationName(extras.getString(KEY_NAME));
			numScans = extras.getInt(KEY_NUMSCANS, DEFAULT_NUM_SCANS);
			wifiScanPeriod = extras.getInt(KEY_WIFI_SCAN_PERIOD, DEFAULT_SCAN_PERIOD);
			startScanning();
			
		}
		if(!dbLoaded)
		{
			
			locations = new HashMap<String, LocationModel>();
			loadModels();
			dbLoaded = true;
		}
		if("check".equalsIgnoreCase(action))
		{
			inChecking = true;
			inTraining = false;
			inRetrain = false;
			
			startScanning();
		}
	
		if("retrain".equalsIgnoreCase(action))
		{
			inChecking = false;
			inTraining = false;
			inRetrain = true;
			String location = extras.getString(KEY_NAME);
			
			long id = extras.getLong(KEY_ID, -1);
			if(id == -1)
			{
				
				id = dbhelper.getLocationId(location);
				if(id == -1)
				{
					return;
				}
			}
			model.setLocationName(location);
			model.setId(id);
			startScanning();
		}
		
	}
	private void startScanning()
	{
		// TODO- Check if location already added
		//String locName = locationName.getText().toString();
		log("Starting scan");
		if(dbhelper == null)
		{
			dbhelper = new LocationDbAdapter(this);
		}
		registerListner();
		scansDone = 0;
		nextScan = new Date();
//		int wifiState = wifiManager.getWifiState();
//		switch(wifiState)
//		{
//		case WifiManager.WIFI_STATE_DISABLING:
//			log("Wifi state disabling");
//			break;
//		case WifiManager.WIFI_STATE_ENABLING:
//			log("Wifi state enabling");
//			break;
//		case WifiManager.WIFI_STATE_DISABLED:
//			log("Wifi state disable");
//			break;
//		case WifiManager.WIFI_STATE_ENABLED:
//			log("Wifi state enable");
//			break;
//		case WifiManager.WIFI_STATE_UNKNOWN:
//			log("Wifi state unknown");
//			break;
//		}
		/* Get lock */
		WifiLock lock = wifiManager.createWifiLock("MLService");
		lock.acquire();
		try {
			/* Sleep some time - wait for wifi to intialise */
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			//e.printStackTrace();
		}
		boolean ret = wifiManager.startScan();
		if(!ret)
		{
			log("Scan could not be initiated !");
		}
		lock.release();
		
	}

	public void registerListner()
	{

		if(!registered)
		{
		registerReceiver(scanResult, scanResultIntentFilter);

		registered = true;
		}
	}


	public void unRegisterListner()
	{
		if(registered)
		{
			unregisterReceiver(scanResult);
			unregisterReceiver(locReceiver);
			registered = false;
		}
	}


	public void onDestroy()
	{
		unRegisterListner();
		dbhelper.close();
		super.onDestroy();
	}
	private void completedScanning() {
		unRegisterListner();
		log( "Completed");
		// We got interrupted before we could complete
		// Or training was not done.
		// Set result as cancelled, return back to caller
		if(scansDone < numScans)
		{
			return;
		}

		// Else we have completed scans successfully
		if(inTraining)
		{
			// Persist model
			long id = persistModel();

			// Show message to user indicating success
			Bundle bundle = retIntent.getExtras();
			bundle.putString(KEY_NAME, model.getLocationName());
			bundle.putLong(KEY_ID, id);
			retIntent.putExtras(bundle);
		
			this.sendBroadcast(retIntent);
			//startService(retIntent);
		}
		/* Without UI - pass back the entire location model */
		else if(inRetrain)
		{
			retrainModel();
		}
		else if(inChecking)
		{
			// Do something 
			testModelAndReturn();

		}
	}
	
	private void retrainModel()
	{
		HashMap <String, WifiNetwork> m = model.getMap();
		
		long id = model.getId();
		if(id < 0)
		{
			return;
		}
		
		dbhelper.deleteLocationModel(id);
		
		for(String bssid : m.keySet())
		{
			WifiNetwork w = m.get(bssid);
			dbhelper.storeSingleModel(id, bssid, w.getAverageVal(), w.getMaxValue(), w.getMinValue());
		}
		
		// Show message to user indicating success
		Bundle bundle = retIntent.getExtras();
		bundle.putString(KEY_NAME, model.getLocationName());
		bundle.putLong(KEY_ID, id);
		retIntent.putExtras(bundle);
	
		this.sendBroadcast(retIntent);
	
	}
	private long persistModel() {
		if(dbhelper == null)
		{
			dbhelper = new LocationDbAdapter(this);
			dbhelper.open();
		}

		//LocationDbAdapter dbhelper = new LocationDbAdapter(this);
		//dbhelper.open();
		HashMap <String, WifiNetwork> m = model.getMap();
		String locationName = model.getLocationName();
		if(locationName == null || "".equals(locationName))
		{
			locationName = genName();
			model.setLocationName(locationName);
		}
		
		long id = dbhelper.addLocation(locationName);
		if(id < 0)
		{
			Log.e("persistData","Error adding to db");
			return -1;
		}
		for(String bssid : m.keySet())
		{
			WifiNetwork w = m.get(bssid);
			dbhelper.storeSingleModel(id, bssid, w.getAverageVal(), w.getMaxValue(), w.getMinValue());
		}
		//dbhelper.close();
		this.locations.put(model.getLocationName(), model);
		return id;
	}

	private void loadModels() {

		if(dbhelper == null)
		{
			dbhelper = new LocationDbAdapter(this);
			dbhelper.open();
		}
		
		Cursor c = dbhelper.fetchAllLocations();

		if(c == null || c.getCount() == 0)
		{
			return;
		}

		c.moveToFirst();
		int idCol = c.getColumnIndex(LocationDbAdapter.KEY_ROWID);
		int nameCol = c.getColumnIndex(LocationDbAdapter.KEY_LOCATION_NAME); 
		
		int lastSeen = c.getColumnIndex(LocationDbAdapter.KEY_LASTUSE);
		int cnt = c.getColumnIndex(LocationDbAdapter.KEY_NUMLOCATED);

		do
		{
			long id = c.getLong(idCol);
			String name = c.getString(nameCol);
			if(locations == null)
			{
				locations = new HashMap<String, LocationModel>();
			}
			Date d = new Date(Long.parseLong(c.getString(lastSeen)));

			LocationModel m = loadModelFromDb(id);
			m.setCount(c.getInt(cnt));
			m.setLastSeen(d);

			locations.put(name, m);

		}while(c.moveToNext());

		//c.moveToFirst();
		c.close();

	}

	private LocationModel loadModelFromDb(long id) {

		Cursor c = dbhelper.fetchLocationModel(id);
		LocationModel t = new LocationModel();

		if(c == null || c.getCount() == 0)
		{
			//Toast.makeText(this, "No entries for this location",
		//			Toast.LENGTH_SHORT).show();
			return t;
		}
		t.setId(id);

		c.moveToFirst();
		int bssidCol = c.getColumnIndex(LocationDbAdapter.KEY_BSSID);
		int avgCol = c.getColumnIndex(LocationDbAdapter.KEY_AVG_VALUE);
		int maxCol = c.getColumnIndex(LocationDbAdapter.KEY_MAX_VALUE);
		int minCol = c.getColumnIndex(LocationDbAdapter.KEY_MIN_VALUE);
		int conn = c.getColumnIndex(LocationDbAdapter.KEY_CONNECTED);

		do
		{
			WifiNetwork w = new WifiNetwork(c.getString(bssidCol));

			w.setAverageVal(c.getDouble(avgCol));
			w.setMaxValue(c.getInt(maxCol));
			w.setMinValue(c.getInt(minCol));
			w.setConnected(c.getInt(conn));

			t.addNetwork(w);
		}while(c.moveToNext());
		c.close();
		return t;
	}

	private void testModelAndReturn()
	{
		if(model == null)
		{

			return;
		}

		double maxProb = 0.0;
		double prob = 0.0 ;
		String bestMatch = null;
		long id = -1;
		
		for(String loc : locations.keySet())
		{
			LocationModel mm = locations.get(loc);
			prob = mm.match(model);

			if(prob > maxProb)
			{
				maxProb = prob;
				bestMatch = loc;
				id = mm.getId();
			};
		}

		if( (maxProb < MIN_PROB))
		{
			log("max prob is " + maxProb + " for " + bestMatch);
			bestMatch = null;
			maxProb = 0.0;
		}
		
		
		Bundle extras = retIntent.getExtras();
		extras.putString(KEY_NAME, bestMatch);
		extras.putDouble(KEY_PROB, maxProb);
		extras.putLong(KEY_ID, id);
		if(bestMatch != null)
		{
			dbhelper.incrementAndTouchLocation(id);
			LocationModel m = locations.get(bestMatch);
			m.incrementCount();
			extras.putInt(KEY_COUNT, m.getCount());
			extras.putLong(KEY_LASTSEEN, m.getLastSeen().getTime());
		}
		
		retIntent.putExtras(extras);

		log("Matching is " + bestMatch + " prob " + maxProb);
		//startService(retIntent);
		this.sendBroadcast(retIntent);

	}
	
	public String genName()
	{
		return "BATT" + dbhelper.getMaxId();
	}

	public void log(String message)
	{
		//Log.i("MLService", message);
	}
}
